package chat.server;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

public class Handler implements Runnable
{
    SelectionKey key;

    Map<String, SocketChannel> userVsChannelMap;

    Map<String, Integer> channelVsStatusMap;

    Set<String> loggedInUsersList;

    public Handler(SelectionKey key, Map<String, Integer> channelVsStatusMap,
            Map<String, SocketChannel> userVsChannelMap, Set<String> loggedInUsersList)
    {
        this.key = key;
        this.userVsChannelMap = userVsChannelMap;
        this.channelVsStatusMap = channelVsStatusMap;
        this.loggedInUsersList = loggedInUsersList;
    }

    public void run()
    {
        SocketChannel client = (SocketChannel) key.channel();
        try
        {
            System.out.println("run is called");
            String commandString = readChannel(client);
            System.out.println("commandString=" + commandString);
            Map<String, String> parameterMap = getParamMap(commandString);
            handleActions(client, parameterMap);

        }
        catch (IOException e)
        {
            System.out.println("inside exception");
            e.printStackTrace();
            key.cancel();
            try
            {
                client.close();
            }
            catch (IOException e1)
            {
                e1.printStackTrace();
            }

        }
        finally
        {
            releaseChannelForRead(client);
        }

    }

    private void releaseChannelForRead(SocketChannel client)
    {
        try
        {
            channelVsStatusMap.put(client.getRemoteAddress().toString(), 0);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    private void handleActions(SocketChannel client, Map<String, String> parameterMap)
            throws IOException
    {
        if (parameterMap.get("action").equals(RequestActionCodes.LOGIN_ACTION_CODE))
        {
            login(client, parameterMap.get("username"));
        }
        else if (parameterMap.get("action").equals(RequestActionCodes.SEND_ACTION_CODE))
        {
            sendMessage(
                    userVsChannelMap.get(parameterMap.get("recipient")),
                    getFormattedString(parameterMap.get("msg"), ResponseCodes.SEND_RESPONSE_CODE,
                            parameterMap.get("from")));
        }
        else if (parameterMap.get("action").equals(RequestActionCodes.SENDALL_ACTION_CODE))
        {
            sendMessageToAll(parameterMap.get("msg"), parameterMap.get("from"));
        }
        else if (parameterMap.get("action").equals(RequestActionCodes.LOGOUT_ACTION_CODE))
        {
            logout(client, parameterMap.get("username"));
        }
    }

    private void logout(SocketChannel client, String userName) throws IOException
    {
        String remoteAdress = client.getRemoteAddress().toString();
        userVsChannelMap.remove(userName);
        loggedInUsersList.remove(remoteAdress);
        channelVsStatusMap.remove(remoteAdress);
        key.cancel();
        client.close();
        announceUserChangeEvent(client, userName, ResponseCodes.LOGOUT_RESPONSE_CODE);

    }

    private void sendMessageToAll(String msg, String from) throws IOException
    {
        Collection<SocketChannel> channels = userVsChannelMap.values();
        for (SocketChannel channel : channels)
        {
            if (channel != userVsChannelMap.get(from))
                sendMessage(channel,
                        getFormattedString(msg, ResponseCodes.SEND_RESPONSE_CODE, from));
        }

    }

    private void sendMessage(SocketChannel targetSocketChannel, String msg) throws IOException
    {
        ByteBuffer msgBuffer = ByteBuffer.wrap(msg.getBytes());
        System.out.println("Writing Msg  " + msg + " Channel " + targetSocketChannel);
        targetSocketChannel.write(msgBuffer);
    }

    private void login(SocketChannel client, String userName) throws IOException
    {
        System.out.println("userName :" + userName);
        String clentAddressString = client.getRemoteAddress().toString();
        System.out.println("clentAddressString>> " + clentAddressString);
        if (!loggedInUsersList.contains(clentAddressString))
        {
            if (userVsChannelMap.get(userName) == null)
            {
                userVsChannelMap.put(userName, client);
                loggedInUsersList.add(clentAddressString);
                announceUserChangeEvent(client, userName, ResponseCodes.LOGIN_RESPONSE_CODE);
            }
            else
            {
                sendMessage(client,
                        getFormattedString(userName, ResponseCodes.ALREADY_LOGGED_RESPONSE_CODE));
            }
        }
        else
        {
            sendMessage(client,
                    getFormattedString(userName, ResponseCodes.CHANNEL_IN_USE_RESPONSE_CODE));
            System.out.println("User has already logged in with this address");
        }
    }

    private void announceLoginEvent(SocketChannel client, String userName) throws IOException
    {
        Collection<SocketChannel> userChannels = userVsChannelMap.values();
        for (SocketChannel userChannel : userChannels)
        {
            if (client == userChannel)
            {
                String userNamesCsv = getUserNameCsv(userName);
                if (userNamesCsv != null && !"".equals(userNamesCsv))
                {
                    sendMessage(
                            client,
                            getFormattedString(getUserNameCsv(userName),
                                    ResponseCodes.LOGIN_RESPONSE_CODE));
                }
            }
            else
            {
                sendMessage(userChannel,
                        getFormattedString(userName, ResponseCodes.LOGIN_RESPONSE_CODE));
            }

        }
    }

    private void announceUserChangeEvent(SocketChannel client, String userName, String event)
            throws IOException
    {
        Collection<SocketChannel> userChannels = userVsChannelMap.values();
        for (SocketChannel userChannel : userChannels)
        {
            if (client == userChannel && !ResponseCodes.LOGOUT_RESPONSE_CODE.equals(event))
            {
                String userNamesCsv = getUserNameCsv(userName);
                if (userNamesCsv != null && !"".equals(userNamesCsv))
                {
                    sendMessage(client, getFormattedString(getUserNameCsv(userName), event));
                }
                else
                {
                    sendMessage(
                            client,
                            getFormattedString("First login",
                                    ResponseCodes.FIRST_LOGIN_RESPONSE_CODE));
                }
            }
            else
            {
                sendMessage(userChannel, getFormattedString(userName, event));
            }

        }
    }

    private String getUserNameCsv(String currentUserName) throws IOException
    {
        Set<String> userNames = userVsChannelMap.keySet();
        StringBuffer userNamesCsv = new StringBuffer();
        for (String userName : userNames)
        {
            if (!currentUserName.equals(userName))
            {
                userNamesCsv.append(userName);
                userNamesCsv.append(",");
            }

        }
        return userNamesCsv.length() > 0 ? userNamesCsv.substring(0, userNamesCsv.length() - 1)
                : "";

    }

    private Map<String, String> getParamMap(String commandString)
    {
        StringTokenizer stringTokens = new StringTokenizer(commandString, "=;");
        Map<String, String> parameterMap = new HashMap<String, String>();
        while (stringTokens.hasMoreTokens())
        {
            String key = stringTokens.nextToken();
            String value = stringTokens.nextToken();
            parameterMap.put(key, value);
        }
        return parameterMap;
    }

    private String readChannel(SocketChannel client) throws IOException, CharacterCodingException
    {
        int BUFFER_SIZE = 512;
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        int bytesRead = client.read(buffer);
        if (bytesRead == -1)
        {
            key.cancel();
            client.close();
        }
        buffer.flip();
        Charset charset = Charset.forName("ISO-8859-1");
        CharsetDecoder decoder = charset.newDecoder();
        CharBuffer charBuffer = decoder.decode(buffer);
        String commandString = charBuffer.toString();

        return commandString;
    }

    private String getFormattedString(String msg, String eventName)
    {
        StringBuffer message = new StringBuffer("msg=");
        message.append(msg);
        message.append(";event=" + eventName);
        return message.toString();
    }

    private String getFormattedString(String msg, String eventName, String fromUserName)
    {
        StringBuffer message = new StringBuffer("msg=");
        message.append(msg);
        message.append(";event=" + eventName);
        message.append(";from=" + fromUserName);
        return message.toString();
    }

}
